package dht.simulation;

import java.util.HashMap;
import java.util.PriorityQueue;
import java.util.Random;
import java.util.Vector;

import dht.data.Access;
import dht.data.AccessType;
import dht.data.Message;
import dht.data.MessageType;
import dht.data.MessageUtil;
import dht.statistics.DhtStatistics;
import dse.modules.eln.EarthLikeNetwork;
import dse.modules.eln.EarthLikeNetworkCostCalculator;

public class DhtFs {
	protected MasterNode masterNode;
	protected HashMap<Integer, DataNode> dataNodes;
	protected EarthLikeNetwork network;
	protected EarthLikeNetworkCostCalculator networkCostCalculator;
	protected HashMap<Integer, Access> pendingAccesses;
	protected PriorityQueue<Message> messageQueue;
	protected Random ran;
	public int totalAccess;
	public int totalAccessProcessed;
	public int systemAccessId;

	public DhtFs(EarthLikeNetwork network) {
		this.masterNode = new MasterNode(GlobalVariables.masterId,
				network.maxNodeId());
		this.dataNodes = new HashMap<Integer, DataNode>();
		this.network = network;
		this.networkCostCalculator = new EarthLikeNetworkCostCalculator();
		this.pendingAccesses = new HashMap<Integer, Access>();
		this.messageQueue = new PriorityQueue<Message>();
		this.ran = new Random(GlobalVariables.seed);
		this.totalAccess = 0;
		this.totalAccessProcessed = 0;
		this.systemAccessId = 0;
		initialize();
	}

	private void initialize() {
		for (int i = 1; i <= network.maxNodeId(); ++i) {
			DataNode dataNode = new DataNode(i, masterNode.masterId, 0.0,
					ran.nextDouble() * GlobalVariables.tableUpdateInterval);
			dataNode.routeTable = masterNode.routeTable;
			dataNode.clientRouteTable = masterNode.routeTable;
			// do not use the function, it will make the statistics report wrong
			// dataNode.setRouteTable(masterNode.routeTable);
			// dataNode.setClientRouteTable(masterNode.routeTable);
			dataNodes.put(i, dataNode);
		}
	}

	public Vector<Message> startAccess(Access access) throws DhtException {
		Vector<Message> msgList = new Vector<Message>();
		Message msg = null;
		if (access.accessType == AccessType.READ
				|| access.accessType == AccessType.WRITE) {
			DataNode fromNode = dataNodes.get(access.requesterId);
			msg = fromNode.clientStartAccess(access);
		} else if (access.accessType == AccessType.TABLE_UPDATE) {
			DataNode fromNode = dataNodes.get(access.requesterId);
			msg = fromNode.serverStartAccess(access);
		} else if (access.accessType == AccessType.VNODE_SPLIT) {
			msg = masterNode.masterStartAccess(access);
		}
		if (msg == null) {
			return null;
		}
		msgList.add(msg);
		return msgList;
	}

	public void initAccess(Access access) throws DhtException {
		Message msg = new Message();
		msg.msgType = MessageType.INIT_ACCESS;
		msg.accessId = access.accessId;
		msg.toId = access.requesterId;
		msg.generateTime = access.startTime;
		pendingAccesses.put(access.accessId, access);
		messageQueue.add(msg);
		totalAccess++;
		DhtStatistics.totalAccessCount++;
	}

	public void run(double toTime, boolean additional) throws DhtException {
		if (!additional) {
			Vector<Access> accesses = initTableUpdateAccess(toTime);
			for (Access access : accesses) {
				initAccess(access);
			}
			accesses = initSplitAccess(toTime);
			for (Access access : accesses) {
				initAccess(access);
			}
		}
		while (!messageQueue.isEmpty()) {
			Message msg = messageQueue.peek();
			if (msg.getCurrentTime() >= toTime) {
				break;
			}
			messageQueue.poll();
			Access access = pendingAccesses.get(msg.accessId);
			DataNode toNode = dataNodes.get(msg.toId);
			Vector<Message> msgList = null;
			if (msg.msgType == MessageType.INIT_ACCESS) {
				msgList = startAccess(access);
			} else if (MessageUtil.isServer2ClientMsg(msg)) {
				msgList = toNode.clientProcessMsg(msg, access);
			} else if (MessageUtil.isServer2MasterMsg(msg)) {
				msgList = masterNode.masterProcessMsg(msg, access);
			} else if (MessageUtil.isServer2ServerMsg(msg)) {
				msgList = toNode.serverProcessMsg(msg, access);
			} else if (MessageUtil.isClient2ServerMsg(msg)) {
				msgList = toNode.serverProcessMsg(msg, access);
			} else if (MessageUtil.isClient2MasterMsg(msg)) {
				msgList = masterNode.masterProcessMsg(msg, access);
			} else if (MessageUtil.isMaster2ServerMsg(msg)) {
				msgList = toNode.serverProcessMsg(msg, access);
			} else if (MessageUtil.isMaster2ClientMsg(msg)) {
				msgList = toNode.clientProcessMsg(msg, access);
			} else {
				throw new DhtException("msgType unknown");
			}
			if (msg.msgType != MessageType.INIT_ACCESS) {
				access.messageFlow.add(msg);
			}
			if (msgList != null) {
				for (Message newMsg : msgList) {
					sendMsg(newMsg);
					access.msgSent++;
					messageQueue.add(newMsg);
				}
				// pendingAccesses.put(access.accessId, access);
			} else if (access.messageFlow.size() == access.msgSent) {
				pendingAccesses.remove(access.accessId);
				totalAccessProcessed++;
				DhtStatistics.accessReport(access);
				DhtStatistics.totalAccessProcessedCount++;
				if (!access.valid()) {
					System.err.println("access is invalid");
					access.dump();
					throw new DhtException("access is invalid");
				}
			}
		}
		// masterNode.routeTable.dump();
	}

	public void sendMsg(Message msg) {
		msg.networkCost = networkCostCalculator.cost(network,
				network.lookupNode(msg.fromId), network.lookupNode(msg.toId),
				msg.msgSize);
	}

	private Vector<Access> initTableUpdateAccess(double toTime)
			throws DhtException {
		Vector<Access> accesses = new Vector<Access>();
		for (int i = 1; i <= network.maxNodeId(); ++i) {
			DataNode node = dataNodes.get(i);
			while (node.tableUpdateTime + GlobalVariables.tableUpdateInterval < toTime) {
				node.tableUpdateTime += GlobalVariables.tableUpdateInterval;
				Access access = new Access();
				access.accessId = --systemAccessId;
				access.startTime = node.tableUpdateTime;
				access.accessType = AccessType.TABLE_UPDATE;
				access.requesterId = i;
				accesses.add(access);
			}
		}
		return accesses;
	}

	private Vector<Access> initSplitAccess(double toTime) {
		Vector<Access> accesses = new Vector<Access>();
		while (masterNode.splitTime + GlobalVariables.splitInterval < toTime) {
			masterNode.splitTime += GlobalVariables.splitInterval;
			Access access = new Access();
			access.accessId = --systemAccessId;
			access.startTime = masterNode.splitTime;
			access.accessType = AccessType.VNODE_SPLIT;
			access.requesterId = masterNode.masterId;
			accesses.add(access);
		}
		return accesses;
	}

	// public void write(String entryMacAddress, DataBlock dataBlock) {
	//
	// }
	//
	// public DataBlock read(String entryMacAddress, String blockName) {
	// return null;
	// }
}
